/*
 * Expander.java
 *
 * Created on March 14, 2005, 1:53 PM
 */

package org.infoset.xsdoc;

import java.util.*;
import java.net.*;
import java.io.*;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.Infoset;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.ItemConstructor;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.util.DocumentSource;
import org.infoset.xml.util.XMLConstructor;


/**
 *
 * @author R. Alexander Milowski
 */
public class Expander
{

   static URI schemaNamespace = URI.create("http://www.w3.org/2001/XMLSchema");
   static URI expanderNamespace = URI.create("http://www.infoset.org/Vocabulary/SchemaExpander/2008/1/0");
   static Name NM_EXPANDED_SCHEMA = new Name(expanderNamespace,"expanded-schema");
   static Name NM_LOCAL_NAME = new Name(expanderNamespace,"local-name");
   static Name NM_NAMESPACE = new Name(expanderNamespace,"namespace");
   static Name NM_PREFIXES = new Name(expanderNamespace,"prefixes");
   static Name NM_PREFIX = new Name(expanderNamespace,"prefix");
   static Name NM_ANNOTATION = new Name(schemaNamespace,"annotation");
   DocumentLoader loader;
   
   Map schemas;
   Map elements;
   Map attributes;
   Map types;
   Map included;
   Map<String,URI> prefixes;
   List errors;
   
   /** Creates a new instance of Expander */
   public Expander()
   {
      schemas = new HashMap();
      elements = new HashMap();
      attributes = new HashMap();
      types = new HashMap();
      included = new HashMap();
      errors = new ArrayList();
      prefixes = new TreeMap<String,URI>();
   }
   
   public void setDocumentLoader(DocumentLoader loader) {
      this.loader = loader;
   }
   
   public Iterator getErrors() {
      return errors.iterator();
   }
   
   public void loadSchema(URI location) 
      throws XMLException
   {
      try {
         loadSchema(loader.load(location));
      } catch (IOException ex) {
         errors.add("Cannot open "+location+" due to I/O error: "+ex.getMessage());
      }
   }
   
   public void loadSchema(Document doc) 
      throws XMLException
   {
      loadSchema(doc,null);
   }
   
   protected void loadSchema(Document doc,URI currentNamespace) 
      throws XMLException
   {
      Infoset infoset = InfosetFactory.getDefaultInfoset();
      
      Element schemaElement = doc.getDocumentElement();
      Iterator inscopePrefixes = schemaElement.getNamespaceScope().getPrefixes();
      while (inscopePrefixes.hasNext()) {
         String prefix = inscopePrefixes.next().toString();
         prefixes.put(prefix, schemaElement.getNamespaceScope().getNamespace(prefix));
      }
      String nsValue = schemaElement.getAttributeValue("targetNamespace");
      if (currentNamespace==null) {
         try {
            currentNamespace = nsValue==null ? Name.NO_NAMESPACE : infoset.createNamespace(nsValue);
         } catch (URISyntaxException ex) {
            throw new XMLException("Bad targetNamespace value on document "+schemaElement.getBaseURI(),ex);
         }
      }
      List l = (List)schemas.get(currentNamespace);
      if (l==null) {
         l = new ArrayList();
         schemas.put(currentNamespace,l);
         l.add(schemaElement);
      } else {
         boolean notFound = true;
         for (Iterator toCheck=l.iterator(); notFound && toCheck.hasNext(); ) {
            Element top = (Element)toCheck.next();
            if (top.getBaseURI().equals(schemaElement.getBaseURI())) {
               notFound = false;
            }
         }
         if (notFound) {
            l.add(schemaElement);
         } else {
            return;
         }
      }
      
      for (Iterator<Element> elementChildren = schemaElement.getElementChildren();
           elementChildren.hasNext(); ) {
         Element topLevel = elementChildren.next();
         Name nm = topLevel.getName();
         if (!nm.getNamespaceName().equals(schemaNamespace)) {
            continue;
         }
         String localName = nm.getLocalName();
         if (localName.equals("element")) {
            Name ename = infoset.createName(currentNamespace, topLevel.getAttributeValue("name"));
            if (elements.get(ename)==null) {
               elements.put(ename,topLevel);
               link(topLevel);
            } else {
               errors.add("Element "+ename+" already defined.");
            }
         } else if (localName.equals("attribute")) {
            Name aname = infoset.createName(currentNamespace, topLevel.getAttributeValue("name"));
            if (attributes.get(aname)==null) {
               attributes.put(aname,topLevel);
               link(topLevel);
            } else {
               errors.add("Attribute "+aname+" already defined.");
            }
         } else if (localName.equals("complexType") || localName.equals("simpleType")) {
            Name tname = infoset.createName(currentNamespace, topLevel.getAttributeValue("name"));
            if (types.get(tname)==null) {
               types.put(tname,topLevel);
               link(topLevel);
            } else {
               errors.add("Type "+tname+" already defined.");
            }
         } else if (localName.equals("include")) {
            String schemaLocation = topLevel.getAttributeValue("schemaLocation");
            if (schemaLocation!=null) {
               URI location = topLevel.getBaseURI().resolve(schemaLocation);
               List incl = (List)included.get(currentNamespace);
               if (incl==null || !incl.contains(location)) {
                  if (incl==null) {
                     incl = new ArrayList();
                     included.put(currentNamespace,incl);
                  }
                  incl.add(location);
                  try {
                     Document includeDoc = loader.load(location);
                     loadSchema(includeDoc,currentNamespace);
                  } catch (IOException ex) {
                     errors.add("Cannot open "+location+" for include due to I/O error: "+ex.getMessage());
                  }
               }
            }
         } else if (localName.equals("import")) {
            String schemaLocation = topLevel.getAttributeValue("schemaLocation");
            String importedNamespace = topLevel.getAttributeValue("namespace");
            try {
               URI imported = new URI(importedNamespace);
               if (schemas.get(imported)==null) {
                  if (schemaLocation!=null) {
                     URI location = topLevel.getBaseURI().resolve(schemaLocation);
                     try {
                        Document importDoc = loader.load(location);
                        String checkNamespace = importDoc.getDocumentElement().getAttributeValue("targetNamespace");
                        if (checkNamespace.equals(importedNamespace)) {
                           loadSchema(importDoc,null);
                        } else {
                           errors.add("Imported schema "+importedNamespace+" does not match targetNamespace attribute of document found.");
                        }
                     } catch (IOException ex) {
                        errors.add("Cannot open "+location+" for import due to I/O error: "+ex.getMessage());
                     }
                  } else {
                     errors.add("Cannot import schema "+importedNamespace+" without schemaLocation");
                  }
               }
            } catch (URISyntaxException ex) {
               errors.add("Bad namespace attribute value '"+importedNamespace+"':"+ex.getMessage());
            }
         }
      }
   }
   
   protected void link(Element def) 
   {
      String localName = def.getName().getLocalName();
      if (localName.equals("element")) {
         String typeQName = def.getAttributeValue("type");
         if (typeQName!=null) {
            try {
               Name typeName = def.getNamespaceScope().expandName(typeQName,true);
               setNameLink(def,typeName);
            } catch (XMLException ex) {
               errors.add("Cannot resolve QName "+typeQName+" for type.");
            }
         } else {
            String refQName = def.getAttributeValue("ref");
            if (refQName!=null) {
               try {
                  Name eName = def.getNamespaceScope().expandName(refQName,true);
                  setNameLink(def,eName);
               } catch (XMLException ex) {
                  errors.add("Cannot resolve QName "+refQName+" for type.");
               }
            }
         }
      } else if (localName.equals("attribute")) {
         String typeQName = def.getAttributeValue("type");
         if (typeQName!=null) {
            try {
               Name typeName = def.getNamespaceScope().expandName(typeQName,true);
               setNameLink(def,typeName);
            } catch (XMLException ex) {
               errors.add("Cannot resolve QName "+typeQName+" for type.");
            }
         } else {
            String refQName = def.getAttributeValue("ref");
            if (refQName!=null) {
               try {
                  Name typeName = def.getNamespaceScope().expandName(refQName,true);
                  setNameLink(def,typeName);
               } catch (XMLException ex) {
                  errors.add("Cannot resolve QName "+refQName+" for type.");
               }
            }
         }
      } else if (localName.equals("extension")) {
         String typeQName = def.getAttributeValue("base");
         if (typeQName!=null) {
            try {
               Name typeName = def.getNamespaceScope().expandName(typeQName,true);
               setNameLink(def,typeName);
            } catch (XMLException ex) {
               errors.add("Cannot resolve QName "+typeQName+" for type.");
            }
         }
      } else if (localName.equals("restriction")) {
         String typeQName = def.getAttributeValue("base");
         if (typeQName!=null) {
            try {
               Name typeName = def.getNamespaceScope().expandName(typeQName,true);
               setNameLink(def,typeName);
            } catch (XMLException ex) {
               errors.add("Cannot resolve QName "+typeQName+" for type.");
            }
         }
      }
      processLinkChildren(def.getElementChildren());
   }
   
   protected void processLinkChildren(Iterator<Element> iter) {
      while (iter.hasNext()) {
         link(iter.next());
      }
   }
   
   protected void setNameLink(Element def, Name nm) {
      if (nm.getNamespaceName()!=Name.NO_NAMESPACE) {
         def.setAttributeValue(NM_NAMESPACE,nm.getNamespaceName().toString());
      }
      def.setAttributeValue(NM_LOCAL_NAME,nm.getLocalName());
      
   }
   
   public void write(ItemDestination dest,URI baseURI)
      throws XMLException
   {
      Infoset infoset = InfosetFactory.getDefaultInfoset();
      ItemConstructor constructor = infoset.createItemConstructor();
      XMLConstructor xmlConstructor = new XMLConstructor(constructor,dest);
      dest.send(constructor.createDocument(baseURI));
      Element top = xmlConstructor.createElement(NM_EXPANDED_SCHEMA);
      top.addNamespaceBinding("ex", expanderNamespace);
      for (String prefix : prefixes.keySet()) {
         top.addNamespaceBinding(prefix,prefixes.get(prefix));
      }
      dest.send(top);
      dest.send(constructor.createCharacters("\n"));
      for (Iterator namespaces=schemas.keySet().iterator(); namespaces.hasNext(); ) {
         URI namespace = (URI)namespaces.next();
         List l = (List)schemas.get(namespace);
         
         Element namespaceElement = xmlConstructor.createElement(NM_NAMESPACE);
         if (namespace!=Name.NO_NAMESPACE) {
            namespaceElement.setAttributeValue("name",namespace.toString());
         }
         dest.send(namespaceElement);
         dest.send(constructor.createCharacters("\n"));
         
         Element annotation = null;
         for (Iterator schemasToCheck=l.iterator(); annotation==null && schemasToCheck.hasNext(); ) {
            Element schemaElement = (Element)schemasToCheck.next();
            for (Iterator<Element> children=schemaElement.getElementChildren(); annotation==null && children.hasNext(); ) {
               Element e= (Element)children.next();
               if (e.getName().equals(NM_ANNOTATION)) {
                  annotation = e;
               }
            }
         }
         if (annotation!=null) {
            DocumentSource.generate(annotation, false, dest);
            dest.send(constructor.createCharacters("\n"));
         }
         
         for (Iterator toCheck=types.keySet().iterator(); toCheck.hasNext(); ) {
            Name name = (Name)toCheck.next();
            if (!name.getNamespaceName().equals(namespace)) {
               continue;
            }
            DocumentSource.generate((Element)types.get(name), false, dest);
            dest.send(constructor.createCharacters("\n"));
         }
         
         for (Iterator toCheck=elements.keySet().iterator(); toCheck.hasNext(); ) {
            Name name = (Name)toCheck.next();
            if (!name.getNamespaceName().equals(namespace)) {
               continue;
            }
            DocumentSource.generate((Element)elements.get(name), false, dest);
            dest.send(constructor.createCharacters("\n"));
         }
         
         for (Iterator toCheck=attributes.keySet().iterator(); toCheck.hasNext(); ) {
            Name name = (Name)toCheck.next();
            if (!name.getNamespaceName().equals(namespace)) {
               continue;
            }
            DocumentSource.generate((Element)attributes.get(name), false, dest);
            dest.send(constructor.createCharacters("\n"));
         }
         xmlConstructor.flushToElement(namespaceElement);
            dest.send(constructor.createCharacters("\n"));
      }
      dest.send(xmlConstructor.createElement(NM_PREFIXES));
      for (String prefix : prefixes.keySet()) {
         Element map = constructor.createElement(NM_PREFIX);
         map.setAttributeValue("prefix",prefix);
         map.setAttributeValue("namespace",prefixes.get(prefix).toString());
         dest.send(map);
         dest.send(constructor.createElementEnd(NM_PREFIX));
      }
      xmlConstructor.flush();
      dest.send(constructor.createCharacters("\n"));
      dest.send(constructor.createDocumentEnd());
   }
   
}
